{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Use OSMnx to calculate street network indicators\n",
    "\n",
    "Author: [Geoff Boeing](https://geoffboeing.com/)\n",
    "\n",
    "  - [Documentation](https://osmnx.readthedocs.io/)\n",
    "  - [Journal article and citation info](https://geoffboeing.com/publications/osmnx-paper/)\n",
    "  - [Code repository](https://github.com/gboeing/osmnx)\n",
    "  - [Examples gallery](https://github.com/gboeing/osmnx-examples)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import networkx as nx\n",
    "import osmnx as ox\n",
    "import pandas as pd\n",
    "\n",
    "ox.__version__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calculate basic street network measures (topological and geometric)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the network for Piedmont, calculate its basic stats, then show the average circuity\n",
    "stats = ox.stats.basic_stats(ox.graph.graph_from_place(\"Piedmont, California, USA\"))\n",
    "stats[\"circuity_avg\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To calculate density-based stats, you must also pass the network's bounding area in square meters (otherwise basic_stats() will just skip them in the calculation):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# get the street network for a place, and its area in square meters\n",
    "place = \"Piedmont, California, USA\"\n",
    "gdf = ox.geocoder.geocode_to_gdf(place)\n",
    "area = ox.projection.project_gdf(gdf).union_all().area\n",
    "G = ox.graph.graph_from_place(place, network_type=\"drive\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# calculate basic and extended network stats, merge them together, and display\n",
    "stats = ox.stats.basic_stats(G, area=area)\n",
    "pd.Series(stats)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Streets/intersection counts and proportions are nested dicts inside the stats dict. To convert these stats to a pandas dataframe (to compare/analyze multiple networks against each other), just unpack these nested dicts first:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# unpack dicts into individiual keys:values\n",
    "stats = ox.stats.basic_stats(G, area=area)\n",
    "for k, count in stats[\"streets_per_node_counts\"].items():\n",
    "    stats[f\"{k}way_int_count\"] = count\n",
    "for k, proportion in stats[\"streets_per_node_proportions\"].items():\n",
    "    stats[f\"{k}way_int_prop\"] = proportion\n",
    "\n",
    "# delete the no longer needed dict elements\n",
    "del stats[\"streets_per_node_counts\"]\n",
    "del stats[\"streets_per_node_proportions\"]\n",
    "\n",
    "# load as a pandas dataframe\n",
    "pd.DataFrame(pd.Series(stats, name=\"value\")).round(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Inspect betweenness centrality"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# calculate betweenness with a digraph of G (ie, no parallel edges)\n",
    "bc = nx.betweenness_centrality(ox.convert.to_digraph(G), weight=\"length\")\n",
    "max_node, max_bc = max(bc.items(), key=lambda x: x[1])\n",
    "max_node, max_bc"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the city of Piedmont, California, the node with the highest betweenness centrality has ~31% of all shortest paths running through it. Let's highlight it in the plot:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nc = [\"r\" if node == max_node else \"w\" for node in G.nodes]\n",
    "ns = [80 if node == max_node else 15 for node in G.nodes]\n",
    "fig, ax = ox.plot.plot_graph(G, node_size=ns, node_color=nc, node_zorder=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "~30% of all shortest paths run through the node highlighted in red. Let's look at the relative betweenness centrality of every node in the graph:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# add the betweenness centraliy values as new node attributes, then plot\n",
    "nx.set_node_attributes(G, bc, \"bc\")\n",
    "nc = ox.plot.get_node_colors_by_attr(G, \"bc\", cmap=\"plasma\")\n",
    "fig, ax = ox.plot.plot_graph(\n",
    "    G,\n",
    "    node_color=nc,\n",
    "    node_size=30,\n",
    "    node_zorder=2,\n",
    "    edge_linewidth=0.2,\n",
    "    edge_color=\"w\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Above, the nodes are visualized by betweenness centrality, from low (dark violet) to high (light yellow). The colors in the colorspace are linearly mapped to the attribute values."
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python (ox)",
   "language": "python",
   "name": "ox"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
